﻿/*
 * Greenshot - a free and open source screenshot tool
 * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom
 * 
 * For more information see: https://getgreenshot.org/
 * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 1 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

using log4net;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using Greenshot.Base.Core;
using Greenshot.Base.IniFile;
using Greenshot.Base.Interfaces;
using Greenshot.Base.Interfaces.Plugin;

namespace Greenshot.Helpers
{
    /// <summary>
    /// Author: Andrew Baker
    /// Datum: 10.03.2006
    /// Available from <a href="https://www.vbusers.com/codecsharp/codeget.asp?ThreadID=71&PostID=1">here</a>
    /// </summary>
    /// <summary>
    /// Represents an email message to be sent through MAPI.
    /// </summary>
    public class MapiMailMessage : IDisposable
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(MapiMailMessage));
        private static readonly CoreConfiguration CoreConfig = IniConfig.GetIniSection<CoreConfiguration>();

        /// <summary>
        /// Helper Method for creating an Email with Attachment
        /// </summary>
        /// <param name="fullPath">Path to file</param>
        /// <param name="title"></param>
        public static void SendImage(string fullPath, string title)
        {
            using MapiMailMessage message = new MapiMailMessage(title, null);
            message.Files.Add(fullPath);
            if (!string.IsNullOrEmpty(CoreConfig.MailApiTo))
            {
                message.Recipients.Add(new Recipient(CoreConfig.MailApiTo, RecipientType.To));
            }

            if (!string.IsNullOrEmpty(CoreConfig.MailApiCC))
            {
                message.Recipients.Add(new Recipient(CoreConfig.MailApiCC, RecipientType.CC));
            }

            if (!string.IsNullOrEmpty(CoreConfig.MailApiBCC))
            {
                message.Recipients.Add(new Recipient(CoreConfig.MailApiBCC, RecipientType.BCC));
            }

            message.ShowDialog();
        }


        /// <summary>
        /// Helper Method for creating an Email with Image Attachment
        /// </summary>
        /// <param name="surface">The image to send</param>
        /// <param name="captureDetails">ICaptureDetails</param>
        public static void SendImage(ISurface surface, ICaptureDetails captureDetails)
        {
            string tmpFile = ImageIO.SaveNamedTmpFile(surface, captureDetails, new SurfaceOutputSettings());

            if (tmpFile == null) return;

            // Store the list of currently active windows, so we can make sure we show the email window later!
            var windowsBefore = WindowDetails.GetVisibleWindows();
            SendImage(tmpFile, captureDetails.Title);
            WindowDetails.ActiveNewerWindows(windowsBefore);
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        private class MapiFileDescriptor
        {
            public int reserved = 0;
            public int flags = 0;
            public int position;
            public string path;
            public string name;
            public IntPtr type = IntPtr.Zero;
        }

        /// <summary>
        /// Specifies the valid RecipientTypes for a Recipient.
        /// </summary>
        public enum RecipientType
        {
            /// <summary>
            /// Recipient will be in the TO list.
            /// </summary>
            To = 1,

            /// <summary>
            /// Recipient will be in the CC list.
            /// </summary>
            CC = 2,

            /// <summary>
            /// Recipient will be in the BCC list.
            /// </summary>
            BCC = 3
        };

        private readonly ManualResetEvent _manualResetEvent;

        /// <summary>
        /// Creates a blank mail message.
        /// </summary>
        public MapiMailMessage()
        {
            Files = new List<string>();
            Recipients = new RecipientCollection();
            _manualResetEvent = new ManualResetEvent(false);
        }

        /// <summary>
        /// Creates a new mail message with the specified subject and body.
        /// </summary>
        public MapiMailMessage(string subject, string body) : this()
        {
            Subject = subject;
            Body = body;
        }

        /// <summary>
        /// Gets or sets the subject of this mail message.
        /// </summary>
        public string Subject { get; set; }

        /// <summary>
        /// Gets or sets the body of this mail message.
        /// </summary>
        public string Body { get; set; }

        /// <summary>
        /// Gets the recipient list for this mail message.
        /// </summary>
        public RecipientCollection Recipients { get; private set; }

        /// <summary>
        /// Gets the file list for this mail message.
        /// </summary>
        public List<string> Files { get; }

        /// <summary>
        /// Displays the mail message dialog asynchronously.
        /// </summary>
        public void ShowDialog()
        {
            // Create the mail message in an STA thread
            var thread = new Thread(ShowMail)
            {
                IsBackground = true,
                Name = "Create MAPI mail"
            };
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();

            // only return when the new thread has built it's interop representation
            _manualResetEvent.WaitOne();
            _manualResetEvent.Reset();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposing)
            {
                return;
            }

            _manualResetEvent?.Close();
        }

        /// <summary>
        /// Sends the mail message.
        /// </summary>
        private void ShowMail()
        {
            while (true)
            {
                var message = new MapiHelperInterop.MapiMessage();

                using var interopRecipients = Recipients.GetInteropRepresentation();
                message.Subject = Subject;
                message.NoteText = Body;

                message.Recipients = interopRecipients.Handle;
                message.RecipientCount = Recipients.Count;

                // Check if we need to add attachments
                if (Files.Count > 0)
                {
                    // Add attachments
                    message.Files = AllocAttachments(out message.FileCount);
                }

                // Signal the creating thread (make the remaining code async)
                _manualResetEvent.Set();

                const int MAPI_DIALOG = 0x8;
                //const int MAPI_LOGON_UI = 0x1;
                int error = MapiHelperInterop.MAPISendMail(IntPtr.Zero, IntPtr.Zero, message, MAPI_DIALOG, 0);

                if (Files.Count > 0)
                {
                    // Deallocate the files
                    DeallocFiles(message);
                }

                MAPI_CODES errorCode = (MAPI_CODES) Enum.ToObject(typeof(MAPI_CODES), error);

                // Check for error
                if (errorCode == MAPI_CODES.SUCCESS || errorCode == MAPI_CODES.USER_ABORT)
                {
                    return;
                }

                string errorText = GetMapiError(errorCode);
                Log.Error("Error sending MAPI Email. Error: " + errorText + " (code = " + errorCode + ").");
                MessageBox.Show(errorText, "Mail (MAPI) destination", MessageBoxButtons.OK, MessageBoxIcon.Error);
                // Recover from bad settings, show again
                if (errorCode != MAPI_CODES.INVALID_RECIPS)
                {
                    return;
                }

                Recipients = new RecipientCollection();
            }
        }

        /// <summary>
        /// Deallocates the files in a message.
        /// </summary>
        /// <param name="message">The message to deallocate the files from.</param>
        private void DeallocFiles(MapiHelperInterop.MapiMessage message)
        {
            if (message.Files == IntPtr.Zero) return;

            Type fileDescType = typeof(MapiFileDescriptor);
            int fsize = Marshal.SizeOf(fileDescType);

            // Get the ptr to the files
            IntPtr runptr = message.Files;
            // Release each file
            for (int i = 0; i < message.FileCount; i++)
            {
                Marshal.DestroyStructure(runptr, fileDescType);
                runptr = new IntPtr(runptr.ToInt64() + fsize);
            }

            // Release the file
            Marshal.FreeHGlobal(message.Files);
        }

        /// <summary>
        /// Allocates the file attachments
        /// </summary>
        /// <param name="fileCount"></param>
        /// <returns></returns>
        private IntPtr AllocAttachments(out int fileCount)
        {
            fileCount = 0;
            if (Files == null)
            {
                return IntPtr.Zero;
            }

            if ((Files.Count <= 0) || (Files.Count > 100))
            {
                return IntPtr.Zero;
            }

            Type atype = typeof(MapiFileDescriptor);
            int asize = Marshal.SizeOf(atype);
            IntPtr ptra = Marshal.AllocHGlobal(Files.Count * asize);

            MapiFileDescriptor mfd = new MapiFileDescriptor
            {
                position = -1
            };
            IntPtr runptr = ptra;
            foreach (string path in Files)
            {
                mfd.name = Path.GetFileName(path);
                mfd.path = path;
                Marshal.StructureToPtr(mfd, runptr, false);
                runptr = new IntPtr(runptr.ToInt64() + asize);
            }

            fileCount = Files.Count;
            return ptra;
        }

        private enum MAPI_CODES
        {
            SUCCESS = 0,
            USER_ABORT = 1,
            FAILURE = 2,
            LOGIN_FAILURE = 3,
            DISK_FULL = 4,
            INSUFFICIENT_MEMORY = 5,
            BLK_TOO_SMALL = 6,
            TOO_MANY_SESSIONS = 8,
            TOO_MANY_FILES = 9,
            TOO_MANY_RECIPIENTS = 10,
            ATTACHMENT_NOT_FOUND = 11,
            ATTACHMENT_OPEN_FAILURE = 12,
            ATTACHMENT_WRITE_FAILURE = 13,
            UNKNOWN_RECIPIENT = 14,
            BAD_RECIPTYPE = 15,
            NO_MESSAGES = 16,
            INVALID_MESSAGE = 17,
            TEXT_TOO_LARGE = 18,
            INVALID_SESSION = 19,
            TYPE_NOT_SUPPORTED = 20,
            AMBIGUOUS_RECIPIENT = 21,
            MESSAGE_IN_USE = 22,
            NETWORK_FAILURE = 23,
            INVALID_EDITFIELDS = 24,
            INVALID_RECIPS = 25,
            NOT_SUPPORTED = 26,
            NO_LIBRARY = 999,
            INVALID_PARAMETER = 998
        }

        /// <summary>
        /// Logs any Mapi errors.
        /// </summary>
        private string GetMapiError(MAPI_CODES errorCode)
        {
            string error = errorCode switch
            {
                MAPI_CODES.USER_ABORT => "User Aborted.",
                MAPI_CODES.FAILURE => "MAPI Failure.",
                MAPI_CODES.LOGIN_FAILURE => "Login Failure.",
                MAPI_CODES.DISK_FULL => "MAPI Disk full.",
                MAPI_CODES.INSUFFICIENT_MEMORY => "MAPI Insufficient memory.",
                MAPI_CODES.BLK_TOO_SMALL => "MAPI Block too small.",
                MAPI_CODES.TOO_MANY_SESSIONS => "MAPI Too many sessions.",
                MAPI_CODES.TOO_MANY_FILES => "MAPI too many files.",
                MAPI_CODES.TOO_MANY_RECIPIENTS => "MAPI too many recipients.",
                MAPI_CODES.ATTACHMENT_NOT_FOUND => "MAPI Attachment not found.",
                MAPI_CODES.ATTACHMENT_OPEN_FAILURE => "MAPI Attachment open failure.",
                MAPI_CODES.ATTACHMENT_WRITE_FAILURE => "MAPI Attachment Write Failure.",
                MAPI_CODES.UNKNOWN_RECIPIENT => "MAPI Unknown recipient.",
                MAPI_CODES.BAD_RECIPTYPE => "MAPI Bad recipient type.",
                MAPI_CODES.NO_MESSAGES => "MAPI No messages.",
                MAPI_CODES.INVALID_MESSAGE => "MAPI Invalid message.",
                MAPI_CODES.TEXT_TOO_LARGE => "MAPI Text too large.",
                MAPI_CODES.INVALID_SESSION => "MAPI Invalid session.",
                MAPI_CODES.TYPE_NOT_SUPPORTED => "MAPI Type not supported.",
                MAPI_CODES.AMBIGUOUS_RECIPIENT => "MAPI Ambiguous recipient.",
                MAPI_CODES.MESSAGE_IN_USE => "MAPI Message in use.",
                MAPI_CODES.NETWORK_FAILURE => "MAPI Network failure.",
                MAPI_CODES.INVALID_EDITFIELDS => "MAPI Invalid edit fields.",
                MAPI_CODES.INVALID_RECIPS => "MAPI Invalid Recipients.",
                MAPI_CODES.NOT_SUPPORTED => "MAPI Not supported.",
                MAPI_CODES.NO_LIBRARY => "MAPI No Library.",
                MAPI_CODES.INVALID_PARAMETER => "MAPI Invalid parameter.",
                _ => string.Empty
            };
            return error;
        }

        /// <summary>
        /// Internal class for calling MAPI APIs
        /// </summary>
        internal class MapiHelperInterop
        {
            /// <summary>
            /// Private constructor.
            /// </summary>
            private MapiHelperInterop()
            {
                // Intenationally blank
            }


            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public class MapiMessage
            {
                public int Reserved = 0;
                public string Subject;
                public string NoteText;
                public string MessageType = null;
                public string DateReceived = null;
                public string ConversationID = null;
                public int Flags = 0;
                public IntPtr Originator = IntPtr.Zero;
                public int RecipientCount;
                public IntPtr Recipients = IntPtr.Zero;
                public int FileCount;
                public IntPtr Files = IntPtr.Zero;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public class MapiRecipDesc
            {
                public int Reserved = 0;
                public int RecipientClass;
                public string Name;
                public string Address;
                public int eIDSize = 0;
                public IntPtr EntryID = IntPtr.Zero;
            }

            [DllImport("MAPI32.DLL", SetLastError = true, CharSet = CharSet.Ansi)]
            public static extern int MAPISendMail(IntPtr session, IntPtr hWnd, MapiMessage message, int flg, int rsv);
        }
    }

    /// <summary>
    /// Represents a Recipient for a MapiMailMessage.
    /// </summary>
    public class Recipient
    {
        /// <summary>
        /// The email address of this recipient.
        /// </summary>
        public string Address;

        /// <summary>
        /// The display name of this recipient.
        /// </summary>
        public string DisplayName;

        /// <summary>
        /// How the recipient will receive this message (To, CC, BCC).
        /// </summary>
        public MapiMailMessage.RecipientType RecipientType = MapiMailMessage.RecipientType.To;

        /// <summary>
        /// Creates a new recipient with the specified address and recipient type.
        /// </summary>
        public Recipient(string address, MapiMailMessage.RecipientType recipientType)
        {
            Address = address;
            RecipientType = recipientType;
        }

        /// <summary>
        /// Returns an interop representation of a recepient.
        /// </summary>
        /// <returns></returns>
        internal MapiMailMessage.MapiHelperInterop.MapiRecipDesc GetInteropRepresentation()
        {
            MapiMailMessage.MapiHelperInterop.MapiRecipDesc interop = new MapiMailMessage.MapiHelperInterop.MapiRecipDesc();

            if (DisplayName == null)
            {
                interop.Name = Address;
            }
            else
            {
                interop.Name = DisplayName;
                interop.Address = Address;
            }

            interop.RecipientClass = (int) RecipientType;

            return interop;
        }
    }

    /// <summary>
    /// Represents a collection of recipients for a mail message.
    /// </summary>
    public class RecipientCollection : CollectionBase
    {
        /// <summary>
        /// Adds the specified recipient to this collection.
        /// </summary>
        public void Add(Recipient value)
        {
            List.Add(value);
        }

        internal InteropRecipientCollection GetInteropRepresentation()
        {
            return new InteropRecipientCollection(this);
        }

        /// <summary>
        /// Struct which contains an interop representation of a colleciton of recipients.
        /// </summary>
        internal struct InteropRecipientCollection : IDisposable
        {
            private int _count;

            /// <summary>
            /// Default constructor for creating InteropRecipientCollection.
            /// </summary>
            /// <param name="outer"></param>
            public InteropRecipientCollection(RecipientCollection outer)
            {
                _count = outer.Count;

                if (_count == 0)
                {
                    Handle = IntPtr.Zero;
                    return;
                }

                // allocate enough memory to hold all recipients
                int size = Marshal.SizeOf(typeof(MapiMailMessage.MapiHelperInterop.MapiRecipDesc));
                Handle = Marshal.AllocHGlobal(_count * size);

                // place all interop recipients into the memory just allocated
                IntPtr ptr = Handle;
                foreach (Recipient native in outer)
                {
                    MapiMailMessage.MapiHelperInterop.MapiRecipDesc interop = native.GetInteropRepresentation();

                    // stick it in the memory block
                    Marshal.StructureToPtr(interop, ptr, false);
                    ptr = new IntPtr(ptr.ToInt64() + size);
                }
            }

            public IntPtr Handle { get; private set; }

            /// <summary>
            /// Disposes of resources.
            /// </summary>
            public void Dispose()
            {
                if (Handle == IntPtr.Zero) return;

                Type type = typeof(MapiMailMessage.MapiHelperInterop.MapiRecipDesc);
                int size = Marshal.SizeOf(type);

                // destroy all the structures in the memory area
                IntPtr ptr = Handle;
                for (int i = 0; i < _count; i++)
                {
                    Marshal.DestroyStructure(ptr, type);
                    ptr = new IntPtr(ptr.ToInt64() + size);
                }

                // free the memory
                Marshal.FreeHGlobal(Handle);

                Handle = IntPtr.Zero;
                _count = 0;
            }
        }
    }
}